type Fn<T extends any[]> = (...args: T) => any;

type HandleFn = (options: {
  /** 当前元素 */
  item: HTMLElement;
  /** 当前元素信息 */
  itemInfo: WeakValue;
}) => void;

interface WeakValue {
  /** 初始状态子元素的信息 */
  first?: DOMRect;
  /** 结束状态子元素的信息 */
  last?: DOMRect;
  /** 是否已监听 */
  transitionend?: boolean;
}

interface InvertParams {
  /** 当前变化的元素 */
  transform?: InvertParamsFn;
  /** 单个动画结束回调（每个元素动画结束都会执行） */
  callback?: InvertParamsFn;
  /** 整体动画结束回调 */
  complete?: () => void;
}

type InvertParamsFn = (element: HTMLElement) => void;

interface FlipThis {
  /** 记录初始状态（通常是页面元素的位置和尺寸） */
  first: () => void;
  /** 使元素从反转的状态过渡到结束状态，会自动记录结束状态 */
  play: (params?: InvertParams) => void;
}

/**
 * 判断是否是函数，并执行
 */
const handleFn = <T extends any[], R>(fn: ((...args: T) => R) | null | undefined, ...args: T) => {
  typeof fn === 'function' && fn(...args);
};

/**
 * 防抖
 * @param fn 函数
 * @param wait 等待时间（默认值：50，单位：ms）
 */
const debounce = <T extends any[]>(fn: Fn<T>, wait = 50): Fn<T> => {
  let time: ReturnType<typeof setTimeout> | null;
  return (...args: T) => {
    time !== null && clearTimeout(time);
    time = setTimeout(() => fn(...args), wait);
  };
};

/**
 * 检查值
 */
function check(value = {} as DOMRect, type: 'left' | 'top') {
  return value[type] || 0;
}

/**
 * 计算位置差值
 */
function calc(first?: DOMRect, last?: DOMRect) {
  const x1 = check(first, 'left');
  const x2 = check(last, 'left');
  const y1 = check(first, 'top');
  const y2 = check(last, 'top');
  return { x: x1 && x2 ? x1 - x2 : 0, y: y1 && y2 ? y1 - y2 : 0 };
}

/**
 * 高效的动画技术，平滑的动画效果
 *
 * @param container 父容器元素
 * @param duration 动画持续时间（默认值：500，单位：ms）
 */
export default function Flip(this: FlipThis, container: Element, duration = 500) {
  // 校验 container 是否有效
  if (!(container instanceof Element)) {
    throw new TypeError('container 必须是有效的 DOM 元素！');
  }

  /**
   * 子元素列表信息
   */
  const childrenInfo: WeakMap<WeakKey, WeakValue> = new WeakMap();

  /**
   * 通用逻辑
   */
  function handle(fn: HandleFn) {
    const list = container.children;
    for (let i = 0; i < list.length; i++) {
      const item = list[i] as HTMLElement;
      const itemInfo = childrenInfo.get(item) || {};
      fn({ item, itemInfo });
    }
  }

  /**
   * 获取所有子元素的信息
   */
  function getChildrenInfo(type: 'first' | 'last') {
    handle(({ item, itemInfo }) => childrenInfo.set(item, { ...itemInfo, [type]: item.getBoundingClientRect() }));
  }

  /**
   * 记录初始状态（通常是页面元素的位置和尺寸）
   */
  this.first = function () {
    getChildrenInfo('first');
  };

  /**
   * 应用结束状态的样式，同时记录此时的状态
   */
  function last() {
    getChildrenInfo('last');
  }

  /**
   * 计算出从初始状态转变到结束状态需要进行的变化，并将这个变化暂时反转（即倒回到初始状态）
   */
  function invert(params?: InvertParams) {
    const { transform, callback, complete = () => {} } = params || {};
    // 自动记录结束状态
    last();
    const completeFn = debounce(complete);
    handle(({ item, itemInfo }) => {
      const { first, last, transitionend } = itemInfo;
      if (first || last) {
        const { x, y } = calc(first, last);
        if (x !== 0 || y !== 0) handleFn(transform, item);
        const preCSS = item.style.cssText;
        item.style.cssText = `transform:translate(${x}px,${y}px);`;
        requestAnimationFrame(() => {
          item.style.cssText = `transition:transform ${duration}ms;${preCSS}`;
          if (!transitionend) {
            item.addEventListener(
              'transitionend',
              debounce((e) => {
                if (e.propertyName === 'transform') {
                  completeFn();
                  handleFn(callback, item);
                }
              }),
            );
            childrenInfo.set(item, { ...itemInfo, transitionend: true });
          }
        });
      } else {
        console.error('步骤错误！请检查 first() 和 last() 方法是否被正确调用！');
      }
    });
  }

  /**
   * 使元素从反转的状态过渡到结束状态
   */
  this.play = function (params?: InvertParams) {
    invert(params);
  };
}

export { Flip };
